#!/bin/ksh
#
# $OpenBSD: portcheck,v 1.112 2017/05/26 20:55:09 jca Exp $
# Copyright (c) 2013 Vadim Zhukov
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

set -e
set +X
set -u

usage() {
	echo "usage: ${0##*/} [-dNP] [-p portsdir] [-x glob]" >&2
	echo "       ${0##*/} -A [-dP] [-p portsdir] [-x glob] [subdir ...]" >&2
	exit 1
}


############################################################
# Parsing command line options
#

existing_port=true
ignore_cvs=true
plist_checks=true
portsdir=
rootrun=false
debugging=false

ignore_list=; unset ignore_list[0]

while getopts "AdNPp:x:" OPT; do
	case $OPT in
	A)
		$existing_port || usage
		if ! $rootrun; then
			ignore_list[${#ignore_list[@]}]=.cvsignore
			ignore_list[${#ignore_list[@]}]=.fslckout
			ignore_list[${#ignore_list[@]}]=.git
			ignore_list[${#ignore_list[@]}]=.gitignore
			ignore_list[${#ignore_list[@]}]=.hg
			ignore_list[${#ignore_list[@]}]=.hgignore
			ignore_list[${#ignore_list[@]}]=.svn
			ignore_list[${#ignore_list[@]}]=FINISHED
			ignore_list[${#ignore_list[@]}]=INDEX
			ignore_list[${#ignore_list[@]}]=README
			ignore_list[${#ignore_list[@]}]=README.md
			ignore_list[${#ignore_list[@]}]=bulk
			ignore_list[${#ignore_list[@]}]=distfiles
			ignore_list[${#ignore_list[@]}]=infrastructure
			ignore_list[${#ignore_list[@]}]=lost+found
			ignore_list[${#ignore_list[@]}]=mystuff
			ignore_list[${#ignore_list[@]}]=openbsd-wip
			ignore_list[${#ignore_list[@]}]=packages
			ignore_list[${#ignore_list[@]}]=plist
			ignore_list[${#ignore_list[@]}]=pobj
			ignore_list[${#ignore_list[@]}]=tests
			ignore_list[${#ignore_list[@]}]=update
		fi
		rootrun=true
		;;

	d)
		debugging=true
		;;

	N)
		$rootrun && usage
		existing_port=false
		ignore_cvs=false
		;;

	P)
		plist_checks=false
		;;

	p)
		portsdir=$OPTARG
		;;

	x)
		set -A ignore_list -- "${ignore_list[@]}" "$OPTARG"
		;;

	*)
		usage
		;;
	esac
done

if ! $rootrun && [[ -n $portsdir && ${PWD##"$portsdir"} == "$PWD" ]]; then
	cat >&2 <<EOE
${0##*/}: current directory does not seem to be under the
specified root directory: $portsdir.
EOE
	exit 3
fi

shift $(($OPTIND - 1))
(($# > 0)) && ! $rootrun && usage
(($# == 0)) && set -- .

############################################################
# Detect path to root of directory tree of current port(s) and put it
# in $portsdir, unless it was set by user above. As a last resort, we
# use some heuristics based on the commonly used names.
#
# We also have a $pkgpath variable, that represents subdirectory under
# root ports directory where the port(s) will be imported. In case we
# use heuristics for determining $portsdir, we'll set up $pkgpath, too,
# since we would get this info anyway.
#
# In make_args we write PORTSDIR_PATH override, that allows us to run
# even in ports directory that is not on the PORTSDIR_PATH. This is
# useful, for example, when you check your port on cvs.openbsd.org,
# where you cannot just override mk.conf.
#

pkgpath=

if [[ -z $portsdir ]]; then
	# idea from DPB/Vars.pm
	test_mf=$(cat <<EOF
COMMENT = test
CATEGORIES = test
PKGPATH = test/a
DISTNAME = test
PERMIT_PACKAGE_CDROM=Yes
IGNORE=Yes
_MAKEFILE_INC_DONE=Yes
ECHO_MSG=:
.include <bsd.port.mk>
SIGNING_PARAMETERS ?=
EOF
	)
	IFS=:
	set -A portsdir_path -- \
		$( (echo "$test_mf" | make -C / -f - show=PORTSDIR_PATH 2>/dev/null) || true)
	unset IFS
	if ((${#portsdir_path[@]} > 0)); then
		for p in "${portsdir_path[@]}"; do
			if [[ -z $portsdir && ${PWD#"$p"} != "$PWD" ]]; then
				portsdir=$p
			elif [[ -n $portsdir && ${PWD#"$p"} != "$PWD" &&
			     $p > $portsdir ]]; then
				portsdir=$p
			fi
		done
	fi
fi

if [[ -z $portsdir ]]; then
	# heuristics mode ON
	pkgpath=${PWD##*/ports/*(mystuff/|openbsd-wip/)}
	portsdir=${PWD%"/$pkgpath"}
fi

if [[ -z $portsdir ]]; then
	cat >&2 <<EOE
${0##*/}: could not detect root ports directory. Please provide
one with -p option.
EOE
	exit 2
fi

# This way we can run all checks even on cvs.openbsd.org and/or
# when SKIPDIR framework is used
set -A make_args -- \
	SKIPDIR= STARTAFTER= STARTDIR= \
	MASTER_SITE_OPENBSD= \
	PORTSDIR_PATH="$portsdir:$(cd /usr/ports && make -V PORTSDIR_PATH || true)"

if $rootrun; then
	cd -- "$portsdir"
	echo "scanning ports under the $portsdir" >&2
fi

############################################################
# Support for SKIPDIR, STARTDIR and STARTAFTER, see ports(7)
#

SKIPDIR=${SKIPDIR:-}
STARTDIR=${STARTDIR:-}
STARTAFTER=${STARTAFTER:-}
if [[ -n $STARTAFTER ]]; then
	STARTDIR=$STARTAFTER
	SKIPDIR="$SKIPDIR $STARTAFTER"
fi

path_parts_count() {
	(IFS=/; set -- $1; echo $#)
}

# true if directory given should be skipped based on STARTDIR
# and/or SKIPDIR variable
skip_dir() {
	$rootrun || return 1
	local dir=$(readlink -f "$1")
	dir=${dir##$portsdir*(/)}
	local startpartscount=$(path_parts_count "$STARTDIR")
	local dirpartscount=$(path_parts_count "$dir")
	if ((dirpartscount >= startpartscount)); then
		[[ -n $STARTDIR && $dir < $STARTDIR ]] && return 0
	fi
	local d
	for d in $SKIPDIR; do
		[[ $d == "$dir" ]] && return 0
	done
	return 1
}

############################################################
# List of well-known top-level port categories
# in a form usable in pattern matching: "foo|bar|baz"
#

top_level_categories=$(xargs <<EOF | sed -e 's/ /|/g'
archivers
astro
audio
base
benchmarks
biology
books
cad
chinese
comms
converters
databases
devel
editors
education
emulators
fonts
games
geo
graphics
inputmethods
japanese
java
korean
lang
mail
math
meta
misc
multimedia
net
news
perl5
plan9
print
productivity
security
shells
sysutils
telephony
tests
textproc
www
x11
EOF
)

############################################################
# List of variables that should not go into port Makefiles
#

user_settings=$(xargs <<EOF | sed -e 's/ /|/g'
BASELOCALSTATEDIR
BASESYSCONFDIR
BATCH
BUILD_ONCE
BULK
BULK_COOKIES_DIR
CHECKSUM_PACKAGES
CHECK_LIB_DEPENDS
COPTS
CXXOPTS
DISTDIR
ECHO_MSG
ECHO_REORDER
FAKEOBJDIR
FETCH_CMD
FETCH_PACKAGES
FETCH_SYMLINK_DISTFILES
FORCE_UPDATE
FTP_PACKAGES
IGNORE_IS_FATAL
IGNORE_SILENT
INTERACTIVE
LIST_DB
LOCKDIR
LOCK_VERBOSE
MASTER_SITE_BACKUP
MASTER_SITE_OVERRIDE
NO_CHECKSUM
NO_DEPENDS
NO_IGNORE
PACKAGE_REPOSITORY
PKG_ADD
PKG_CREATE
PKG_DBDIR
PKG_DELETE
PKG_INFO
REFETCH
SIGNING_PARAMETERS
SUDO
TEMPLATES
TRY_BROKEN
UNLOCK_CMD
UPDATE_COOKIES_DIR
USE_CCACHE
VARBASE
WARNINGS
EOF
)

############################################################
# Check and fail routines
#

error=false

err() {
	local prefix=
	while (($# > 0)); do
		printf "$prefix%s" "$1" >&2
		prefix=" "
		shift
	done
	echo >&2
	error=true
}

err_duplicated() {
	err "both $2 and some of its parents has $1"
}

err_coredump_found() {
	err "core dump file found: $1"
}

has_subdirs_only() {
	$debugging && echo "CALLED: has_subdirs_only($*)" >&2

	local dir=$1; shift
	ls -A "$dir" | {
		local has_files=false has_dirs=false
		while read F; do
			$ignore_cvs && [[ $F == CVS ]] && continue
			ignoring "$dir/$F" && continue
			if [[ -d $dir/$F ]]; then
				has_dirs=true
			else
				has_files=true
			fi
		done
		$has_dirs && ! $has_files
	}
}

ignoring() {
	((${#ignore_list[*]} > 0)) || return 1
	local iglob
	for iglob in "${ignore_list[@]}"; do
		[[ ${1#./} == $iglob ]] && return 0
	done
	return 1
}

is_vcs_item() {
	[[ -d "$1" && ${1##*/} == @(CVS|.fslckout|.git|.hg|.svn) ]]
}

handle_extra_file() {
	ignoring "$1" && return 0

	# avoid warning, e.g., about ".*"
	test -e "$1" || return 0

	if is_vcs_item "$1"; then
		if ! $ignore_cvs || [[ ${1##*/} != CVS ]]; then
			err "VCS item detected: $1"
		fi
	elif [[ -f $1 && $1 == *.core ]]; then
		err_coredump_found "$1"
	elif [[ -d $1 ]]; then
		err "extra directory: $1"
	else
		err "extra file: $1"
	fi
}

# Make a path to .py[co] file looks like as if it's in the same dir
# as the corresponding .py file, and has same basename. E.g.:
#   lib/python3.3/__pycache__/Foo/cpython-33.Bar.pyc
# became:
#   lib/python2.7/Foo/Bar.pyc
# which corresponds to:
#   lib/python2.7/Foo/Bar.py
normalize_pyco() {
	local pyco=$1
	[[ $pyco == *.cpython-+([0-9]).py[co] ]] &&
		pyco=${pyco%.cpython-+([0-9]).py[co]}.${pyco##*.}
	[[ $pyco == */__pycache__/* ]] &&
		pyco=${pyco%/__pycache__/*}/${pyco##*/__pycache__/}
	printf "%s" "$pyco"
}

# Print out a ref to the particular subport/subpackage, if needed.
# Port FLAVORs could also be handled, if provided.
# Usage: portref directory [subpackage [flavor all_flavors]]
portref() {
	local dir=$1; shift
	local subpkg= flavor all_flavors=
	if (($# > 0)); then
		subpkg=$1
		shift
	fi
	if (($# > 0)); then
		flavor=$1
		all_flavors=$2
		shift 2
	fi

	local ref=
	if [[ $dir != . ]]; then
		ref="${dir#./}"
		[[ -n $subpkg && $subpkg != "-" ]] && ref="$ref,$subpkg"
	else
		[[ $subpkg != "-" ]] && ref="$subpkg"
	fi

	if [[ -n $all_flavors ]]; then
		[[ -n $ref ]] && ref="$ref, "
		if [[ -z $flavor ]]; then
			ref="${ref}default FLAVOR"
		else
			ref="${ref}FLAVOR \"$flavor\""
		fi
	fi

	[[ -n $ref ]] && echo "in $ref: "
}

# Contains last SUBST_CMD. Filled by check_port_dir(), used
# by check_port_hier() to lazily call the check_pkg_dir().
last_subst_cmd=

# Checks made:
#   * Whitelist filter of what could be in this directory.
check_port_hier() {
	$debugging && echo "CALLED: check_port_hier($*)" >&2

	local distinfo_lives_upper pkg_lives_upper plist_lives_upper
	local dir=$1; shift
	for opt; do
		# looks unsafe but we do not pass anything except
		# "foo=true" and "foo=false" here
		eval "$opt"
	done

	distinfo_lives_upper=${distinfo_lives_upper:-false}
	pkg_lives_upper=${pkg_lives_upper:-false}
	plist_lives_upper=${plist_lives_upper:-false}

	local distinfo_exists=false
	[[ -f $dir/distinfo ]] && distinfo_exists=true
	$distinfo_exists && $distinfo_lives_upper &&
		err_duplicated distinfo "$dir"

	local pkg_exists=false tell_pkg_exists=$pkg_lives_upper
	if [[ -d $dir/pkg ]]; then
		pkg_exists=true
		tell_pkg_exists=true
	fi

	local plist_exists=false
	ls $dir/pkg/PLIST* >/dev/null 2>&1 && plist_exists=true
	$plist_lives_upper && $plist_exists &&
		err_duplicated "packing list(s)" "$dir"

	$distinfo_lives_upper && distinfo_exists=true
	$plist_lives_upper && plist_exists=true

	local recursive_args
	set -A recursive_args -- \
		distinfo_lives_upper=$distinfo_exists \
		pkg_lives_upper=$tell_pkg_exists \
		plist_lives_upper=$plist_exists

	local F
	for F in "$dir"/* "$dir"/.*; do
		F=${F#./}
		ignoring "$F" && continue

		if is_vcs_item "$F"; then
			if ! $ignore_cvs || [[ ${F##*/} != CVS ]]; then
				err "VCS item detected: $F"
			fi
		elif [[ -d $F ]]; then
			case "${F##*/}" in
			files|patches)
				check_${F##*/}_dir "$F"
				;;

			pkg)
				# Do nothing, pkg_exists is already set,
				# and we need to read SUBST_CMD first.
				;;

			patches?(-*))
				check_patches_dir "$F"
				;;

			*)
				if ! ([[ -f $F/Makefile ]] ||
                                      ls $F/*.port.mk >/dev/null 2>&1) &&
				   ! has_subdirs_only "$F"; then
					# Avoid extra spam
					err "not a port directory: $F"
				else
					local pkgpath_set=false
					[[ -n $pkgpath ]] && pkgpath_set=true
					check_port_dir "$F" "${recursive_args[@]}"
					$pkgpath_set || pkgpath=${pkgpath%/*}
				fi
				;;
			esac
		else
			case "${F##*/}" in
			Makefile?(.inc)|*.port.mk)
				check_makefile "$F"
				;;

			distinfo)
				;;

			*)
				handle_extra_file "$F"
				;;
			esac
		fi
	done

	$pkg_exists && check_pkg_dir "$dir"/pkg "$last_subst_cmd"

	$existing_port ||
		egrep -q '^ *SUBDIR[[:space:]]*\+?=' "$dir"/Makefile ||
		err missing subdir Makefile
}

# Checks made:
#   * Whitelist filter of what could be in this directory.
check_port_dir() {
	$debugging && echo "CALLED: check_port_dir($*)" >&2

	local dir=$1; shift
	skip_dir "$dir" && return
	local distinfo_lives_upper pkg_lives_upper plist_lives_upper
	for opt; do
		# looks unsafe but we do not pass anything except
		# "foo=true" and "foo=false" here
		eval "$opt"
	done

	distinfo_lives_upper=${distinfo_lives_upper:-false}
	pkg_lives_upper=${pkg_lives_upper:-false}
	plist_lives_upper=${plist_lives_upper:-false}
 
	check_perms_in_dir "$dir"

	if [[ -f $dir/Makefile.inc ]] ||
	   egrep -sq '^ *SUBDIR[[:space:]]*\+?=' "$dir"/Makefile ||
	   has_subdirs_only "$dir"; then
		check_port_hier "${dir#./}" "${@:-}"
		return
	fi

	local F
	local distinfo_exists=false
	local mk_exists=false
	local pkg_exists=false
	local plist_exists=false
	local portmk_exists=true
	local non_portmk=0

	for F in "$dir"/* "$dir"/.*; do
		F=${F#./}
		ignoring "$F" && continue
		case ${F##*/} in
		Makefile)
			test -f "$F" || err "$F is not a file"
			check_makefile "$F"
			mk_exists=true
			((++non_portmk))
			;;

		distinfo)
			$distinfo_lives_upper && err_duplicated distinfo "$dir"
			distinfo_exists=true
			test -f "$F" || err "$F is not a file"
			((++non_portmk))
			;;

		*.port.mk)
			test -f "$F" || err "$F is not a file"
			check_makefile "$F"
			portmk_exists=true
			;;

		files|patches)
			if [[ -d $F ]]; then
				check_${F##*/}_dir "$F"
			else
				err "$F" is not a directory
			fi
			((++non_portmk))
			;;

		pkg)
			if [[ -d $F ]]; then
				pkg_exists=true
				# Actual check to be done later, we need to gather
				# additional info through "make show=" call.
				ls "$F"/PLIST* >/dev/null 2>&1 &&
					plist_exists=true
				$plist_lives_upper && $plist_exists &&
					err_duplicated "packing list(s)" "$dir"
			else
				err "$F" is not a directory
			fi
			((++non_portmk))
			;;

		*)
			handle_extra_file "$F"
			;;
		esac
	done

	# examples: lang/clang, www/mozilla
	$portmk_exists && ((non_portmk == 0)) && return

	$mk_exists || err no Makefile in "$dir"
	$pkg_exists || $pkg_lives_upper || err "no pkg/ in $dir"
	$distinfo_lives_upper && distinfo_exists=true
	$distinfo_exists || $existing_port || err "no distinfo in $dir"

	# Now gather and check some info via "make show=...".
	# We request all info at once for speed.

	local categories dist_subdir distfiles flavor flavors
	local gh_commit master_sites
	local multi_packages pkgpath_this pseudo_flavor pseudo_flavors
	local shared_libs subst_cmd
	local perm_pkg_cdrom perm_pkg_ftp perm_dist_ftp
	local show_items="CATEGORIES DIST_SUBDIR DISTFILES FLAVOR FLAVORS GH_COMMIT"
	local show_items="$show_items MASTER_SITES MULTI_PACKAGES PKGPATH"
	local show_items="$show_items PSEUDO_FLAVOR PSEUDO_FLAVORS"
	local show_items="$show_items SHARED_LIBS SUBST_CMD"
	local show_items="$show_items PERMIT_PACKAGE_CDROM PERMIT_PACKAGE_FTP"
	local show_items="$show_items PERMIT_DISTFILES_FTP"
	local read_ok=false

	local read_failed=false
	(cd -- "$dir"; make "${make_args[@]}" show="$show_items" || true) </dev/null |&
	read -pr categories &&
	read -pr dist_subdir &&
	read -pr distfiles &&
	read -pr flavor &&
	read -pr flavors &&
	read -pr gh_commit &&
	read -pr master_sites &&
	read -pr multi_packages &&
	read -pr pkgpath_this &&
	read -pr pseudo_flavor &&
	read -pr pseudo_flavors &&
	read -pr shared_libs &&
	read -pr subst_cmd &&
	read -pr perm_pkg_cdrom &&
	read -pr perm_pkg_ftp &&
	read -pr perm_dist_ftp &&
	read_ok=true
	if $read_ok; then
		exec 3<&p
		exec 3<&-
		wait
	else
		error=true
		return
	fi

	pseudo_flavor=$(echo "$pseudo_flavor" | sed -e 's/,/ /g')
	pseudo_flavor=${pseudo_flavor##" "}

	local f pf found

	local check_flavors=
	[[ $flavor != "$pseudo_flavor" ]] && unset check_flavors[0]

	for f in $flavors; do
		for pf in $pseudo_flavors; do
			[[ $f == "$pf" ]] && continue 2
		done
		[[ $f == debug ]] && continue     # XXX
		check_flavors[${#check_flavors[@]}]=$f
	done

	check_categories "$dir" $categories
	check_distfiles "$dir" "$dist_subdir" $distfiles
	check_master_sites "$dir" $master_sites
	check_permit_dist "$dir" "$perm_pkg_cdrom" "$perm_pkg_ftp" \
	    "$perm_dist_ftp"
	$pkg_exists && check_pkg_dir "$dir"/pkg "$subst_cmd"
	$existing_port || check_shlibs_versions "$dir" $shared_libs

	if [[ -n $gh_commit ]]; then
		local ghclen=$(echo -n "$gh_commit" | wc -c)
		if ((ghclen != 40)); then
			err "GH_COMMIT should be in full form (40 characters)"
		fi
	fi

	for _s in $multi_packages; do
		sub_checks "$dir" "$_s" "${check_flavors[@]}"
	done

	pkgpath=${pkgpath:-"$pkgpath_this"}
	last_subst_cmd="$subst_cmd"
}

# Checks made: obvious
check_trailing_whitespace() {
	egrep -q '[[:space:]]+$' "$1" &&
		err "trailing whitespace in $1"
}

# Checks made: obvious
check_newline_at_eof() {
	(( $(tail -1 -- "$1" | wc -l) == 0)) &&
		err "no newline at EOF in $1"
}

# Checks made:
#   * Every library in SHARED_LIBS has 0.0 version.
check_shlibs_versions() {
	$debugging && echo "CALLED: check_shlibs_versions($*)" >&2

	local dir=$1; shift
	local lib
	local libver
	local portref=$(portref "$dir")

	while (($# > 1)); do
		lib=$1
		libver=$2
		if [[ $libver != 0.0 ]]; then
			err "${portref}the $lib shared library has" \
			    "version $libver instead of 0.0"
		fi
		shift 2
	done
}

# Checks made:
#   * All top-level category names are well-known.
check_categories() {
	$debugging && echo "CALLED: check_categories($*)" >&2

	local dir=$1; shift
	local portref=$(portref "$dir")
	for c in "$@"; do
		c=${c%%/*}
		if ! echo "$c" | egrep -q "^($top_level_categories)\$"; then
			err "${portref}non-standard top-level category: $c"
		fi
	done
}

# Checks made:
#   * Distfiles with useless names go into DIST_SUBDIR or have {url} suffix.
check_distfiles() {
	$debugging && echo "CALLED: check_distfiles($*)" >&2

	local dir=$1; shift
	local dist_subdir=$1; shift
	local portref=$(portref "$dir")

	# do not care about absent distfiles, this is fine for meta ports
	while (($# > 1)); do
		# try to catch "version-only" names, but not anything more
		if [[ $1 == ?(v)?(.)+([0-9])?(.+([0-9]))*(.+([a-z])) &&
		      -z $dist_subdir && $1 != *\{*\} ]]; then
			err "${portref}badly named distfile $1 without" \
			    "DIST_SUBDIR or {url} postfix"
		fi
		shift
	done
}

# Checks made:
#   * No unreliable (without fixed distfiles) hosting listed in MASTER_SITES.
check_master_sites() {
	$debugging && echo "CALLED: check_master_sites($*)" >&2

	local dir=$1; shift
	local portref=$(portref "$dir")
	local name

	while (($# > 1)); do
		case "$1" in
		http?(s)://bitbucket.com/*)	name=BitBucket;;
		http?(s)://gitorious.com/*)	name=Gitorious;;
		*)				name=;;
		esac
		[[ -n $name ]] && err "$portref$name does not hold real" \
			"releases, please host the distfiles somewhere" \
			"else or ask someone to do this for you"
		shift
	done
}

# Run checks that are FLAVOR/SUBPACKAGE-dependent.
sub_checks() {
	$debugging && echo "CALLED: sub_checks($*)" >&2

	local dir=$1; shift
	local subpkg=$1; shift
	local flavor
	for flavor in "$@"; do
		# avoid extra noise
		[[ ${flavor#no_} != ${flavor} &&
		   ${subpkg#-} == ${flavor#no_} ]] &&
		   continue

		(
			cd -- "$dir"
			portref=$(portref "$dir" "$subpkg" "$flavor" "$*")
			export SUBPACKAGE="$subpkg" FLAVOR="$flavor"

			local wantlib_var=WANTLIB${subpkg%-}
			local vars="COMMENT$subpkg FULLPKGNAME$subpkg"
			vars="$vars MODULES"
			vars="$vars PKG_ARCH$subpkg $wantlib_var WANTLIB-"
			vars="$vars PERMIT_PACKAGE_CDROM${subpkg%-}"
			vars="$vars PERMIT_PACKAGE_FTP${subpkg%-}"
			make "${make_args[@]}" show="$vars" | {
				local comment fullpkgname modules pkg_arch
				local wantlib perm_pkg_cdrom perm_pkg_ftp
				read -r comment
				read -r fullpkgname
				read -r modules
				read -r pkg_arch
				read -r wantlib
				read -r wantlib_ss
				read -r perm_pkg_cdrom
				read -r perm_pkg_ftp

				if [[ $comment == @(a|an|the)" "* ]]; then
					err "${portref}no leading articles in" \
					    "COMMENT${subpkg%-}, please"
				fi

				if [[ $pkg_arch == "*" && -n $wantlib ]]; then
					if [[ $subpkg != - || -n $wantlib_ss ]]; then
						err "${portref}non-empty $wantlib_var for" \
						    "arch-independent package"
					fi
				fi

				check_wantlib "$portref" "$modules" $wantlib
				check_permit_subpkg "$portref" "$subpkg" \
				    "$perm_pkg_cdrom" "$perm_pkg_ftp"

				if $plist_checks; then
					(make "${make_args[@]}" \
					    print-plist-with-depends || true) \
					    </dev/null |&
					check_plist "$portref" "$fullpkgname" \
					    "$flavor" "${subpkg%-}" "$modules"
					check_lib_depends "$portref" "$subpkg" \
					    "$modules" "$wantlib"
					wait
				fi

				! $error
			} || error=true

			! $error
		) || error=true
	done
	wait
}

# Checks made:
#   * If package installs system-wide icons, it should have the
#     x11/gtk+3,-guic dependency and @exec/@unexec-delete with
#     %D/bin/gtk-update-icon-cache -q -t %D/share/icons/$theme
#     for each icon theme used in package. If there is an
#     index.theme provided, then, instead of gtk-update-icon-cache,
#     @unexec-delete should contain the following command:
#     rm -f %D/share/icons/$theme/icon-theme.cache
#
#   * If package adds a MIME type handler, it should have the
#     devel/desktop-file-utils dependency and @exec/@unexec-delete with
#     %D/bin/update-desktop-database . Unfortunately, it's hard to tell
#     if there is a MIME type handler in .desktop file, so we just
#     trigger if any .desktop files are added to
#     ${PREFIX}/share/applications/ .
#
#   * If package adds a MIME types package, it should have the
#     misc/shared-mime-info dependency and @exec/@unexec-delete with
#     %D/bin/update-mime-database %D/share/mime
#
#   * If package adds a GLib schema, it should have @exec/@unexec-delete
#     with %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null
#     and "devel/dconf" in MODULES (or at least RDEP on devel/dconf).
#
#   * If package installs .mo files under ${PREFIX}/share/locale/, then
#     run-time dependency on devel/gettext should exists.
#
#   * If package installs files under ${PREFIX}/share/dbus-1/system-services/,
#     it must have a run-time dependency on x11/dbus,-suid.
#
#   * Each .py should have corresponding .pyc files, to avoid
#     generation of the latter at run-time.
#
#   * Manual (man and info) pages should go under ${PREFIX}/{man,info},
#     not under ${PREFIx}/share/{man,info}.
#
#   * Manual pages shouldn't be compressed.
check_plist() {
	$debugging && echo "CALLED: check_plist($*)" >&2

	local portref=$1; shift
	local fullpkgname=$1; shift
	local flavor_list=$1; shift
	local subpkg=$1; shift
	local modules_list=$1; shift

	local flavor is_static=false
	for flavor in $flavor_list; do
		[[ $flavor == static ]] && is_static=true
	done

	local guic_dep=false
	local guic_dep_needed=false
	local guic_exec_cnt=0
	local guic_unexec_cnt=0

	local mime_dep=false
	local mime_dep_needed=false
	local mime_exec_cnt=0
	local mime_unexec_cnt=0

	local mimepkg_dep=false
	local mimepkg_dep_needed=false
	local mimepkg_exec_cnt=0
	local mimepkg_unexec_cnt=0

	local dconf_module
	if [[ $modules_list == ?(* )devel/dconf?( *) ]]; then
		dconf_module=true
	else
		dconf_module=false
	fi
	local dconf_dep=false
	local dconf_dep_needed=false
	local dconf_exec_cnt=0
	local dconf_unexec_cnt=0

	# Lists of icon themes discovered through reading
	# @file, @exec and @unexec lines, accordingly.
	local icon_themes= exec_icon_themes= unexec_icon_themes=

	# List of icon themes that remove cache file
	local rm_cache_themes=

	local gettext_dep=false
	local translation_found=false

	local dbus_suid_dep=false
	local dbus_suid_dep_needed=false

	# Lists of .py, .pyc and .pyo items found, accordingly
	local py_files=   pyc_files=   pyo_files=
	unset py_files[0] pyc_files[0] pyo_files[0]

	local wrong_man=false wrong_info=false

	local regsh=false unregsh=false
	local regsh_exec=false unregsh_exec=false

	# Temporary ones
	local app l theme varname py

	while read -pr l; do
		case "$l" in
		"@comment "*)
			# ignore
			;;
		share/icons/*/*/*|share/icons/*/@(index.theme|iconrc?(-png)))
			# Themes have at least two levels in depth.
			#
			# We match directories by purpose, this helps to catch
			# update-plist fuckups, when directories go into one
			# package and actual icons go in another.
			guic_dep_needed=true
			theme=${l#share/icons/}
			theme=${theme%%/*}
			# wrap with the '/' characters to avoid erroneous matching
			echo "$icon_themes" | fgrep -q "/$theme/" ||
				icon_themes="$icon_themes /$theme/"
			if [[ "$l" = "share/icons/$theme/index.theme" ]]; then
				echo "$rm_cache_themes" | fgrep -q "/$theme/" ||
					err "${portref}missing @unexec-delete rm -f" \
					    "%D/share/icons/$theme/icon-theme.cache"
			fi
			;;
		share/icons/*(*/))
			# Do not match intermediate directories to avoid false
			# positives.
			;;
		share/icons/*.xpm)
			app=${l#share/icons/}
			app=${app%%/*}
			app=${app%%.*}
			err "${portref}installs icon ${l##*/} in ${l%/*}, it" \
			    "should likely go in share/pixmaps/ instead"
			;;
		share/icons/default.*)
			;;
		share/icons/*)
			app=${l#share/icons/}
			app=${app%%/*}
			app=${app%%.*}
			err "${portref}installs icon ${l##*/} in ${l%/*}, it" \
			    "should go in share/$app/icons/ or like instead"
			;;
		"@depend x11/gtk+3,-guic"*)
			guic_dep=true
			;;
		"@exec %D/bin/gtk-update-icon-cache -q -t %D/share/icons/"*)
			theme=${l##*/}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_exec_cnt))
			eval "((++guic_exec_cnt_$varname))"
			exec_icon_themes="$exec_icon_themes /$theme/"
			;;
		"@unexec-delete %D/bin/gtk-update-icon-cache -q -t %D/share/icons/"*)
			theme=${l##*/}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_unexec_cnt))
			eval "((++guic_unexec_cnt_$varname))"
			unexec_icon_themes="$unexec_icon_themes /$theme/"
			;;
		"@unexec-delete rm -f "%D/share/icons/*/icon-theme.cache)
			# as an alternative, port could zap the theme entirely
			theme=${l#*/icons/}
			theme=${theme%/icon-theme.cache}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_unexec_cnt))
			eval "((++guic_unexec_cnt_$varname))"
			unexec_icon_themes="$unexec_icon_themes /$theme/"
			rm_cache_themes="$rm_cache_themes /$theme/"
			if echo "$icon_themes" | fgrep -q "/$theme/"; then
				err "${portref}the @unexec-delete line removing" \
				    "%D/share/icons/$theme/icon-theme.cache" \
				    "does not preceed all of the icon theme" \
				    "$theme files"
			fi
			;;
		@?(un)exec?(-delete|-update)" %D/bin/gtk-update-icon-cache"*)
			err "${portref}incorrect gtk-update-icon-cache" \
			    "invocation: ${l#@* }"
			;;

		share/applications/*(*/)*.desktop)
			mime_dep_needed=true
			;;
		"@depend devel/desktop-file-utils"*)
			mime_dep=true
			;;
		"@exec %D/bin/update-desktop-database")
			((++mime_exec_cnt))
			;;
		"@unexec-delete %D/bin/update-desktop-database")
			((++mime_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/update-desktop-database"*)
			err "${portref}incorrect update-desktop-database" \
			    "invocation: ${l#@* }"
			;;

		share/mime/packages/*.xml)
			mimepkg_dep_needed=true
			;;
		"@depend misc/shared-mime-info"*)
			mimepkg_dep=true
			;;
		"@exec %D/bin/update-mime-database %D/share/mime")
			((++mimepkg_exec_cnt))
			;;
		"@unexec-delete %D/bin/update-mime-database %D/share/mime")
			((++mimepkg_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/update-mime-database"*)
			err "${portref}incorrect update-mime-database" \
			    "invocation: ${l#@* }"
			;;

		share/glib-2.0/schemas/*.xml)
			dconf_dep_needed=true
			;;
		"@depend devel/dconf"*)
			dconf_dep=true
			;;
		"@exec %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null")
			((++dconf_exec_cnt))
			;;
		"@unexec-delete %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null")
			((++dconf_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/glib-compile-schemas"*)
			err "${portref}incorrect glib-compile-schemas" \
			    "invocation: ${l#@* }"
			;;

		"@depend devel/gettext"*)
			gettext_dep=true
			;;
		share/locale/*/*/*.mo)
			translation_found=true
			;;

		share/dbus-1/system-services/*)
			dbus_suid_dep_needed=true
			;;
		"@depend x11/dbus,-suid"*)
			dbus_suid_dep=true
			;;

		lib/ghc/*/register.sh)
			regsh=true
			;;
		lib/ghc/*/unregister.sh)
			unregsh=true
			;;
		"@exec /usr/bin/env HOME=/nonexistent "%D/lib/ghc/*/register.sh" -v0")
			regsh_exec=true
			;;
		"@unexec /usr/bin/env HOME=/nonexistent "%D/lib/ghc/*/unregister.sh" -v0 --force")
			unregsh_exec=true
			if $unregsh; then
				err "${portref}unregister.sh call placed before" \
				    "script itself in PLIST${subpkg}"
			fi
			;;

		# XXX KSH arrays are limited to 10239 items
		share/@(doc|*(*/)examples)+(/*).py|?(s)bin/*.py)
			# ignore
			;;
		*.py)
			py_files[${#py_files[@]}]=$l
			;;
		*.pyc)
			pyc_files[${#pyc_files[@]}]=$(normalize_pyco "$l")
			;;
		*.pyo)
			pyo_files[${#pyo_files[@]}]=$(normalize_pyco "$l")
			;;

		share/man/*)
			wrong_man=true
			;;
		share/info/*)
			wrong_info=true
			;;
		"@man "*.gz)
			err "${portref}compressed $l"
			;;
		esac
	done

	# gtk-update-icon-cache
	$guic_dep_needed && ! $guic_dep &&
	    [[ $fullpkgname != gtk-update-icon-cache-* ]] &&
		err "${portref}missing RDEP on x11/gtk+3,-guic"
	local cnt
	for theme in $icon_themes; do
		theme=${theme#/}
		theme=${theme%/}

		varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')

		((guic_exec_cnt--)) || true
		((guic_unexec_cnt--)) || true
		eval "((guic_exec_cnt_$varname--)) || true"
		eval "((guic_unexec_cnt_$varname--)) || true"

		eval "cnt=\$guic_exec_cnt_$varname"
		if (($cnt > 0)); then
			err "${portref}extra @exec of gtk-update-icon-cache" \
			    "for icon theme $theme"
			((guic_exec_cnt--)) || true
		elif (($cnt < 0)); then 
			err "${portref}missing @exec of gtk-update-icon-cache" \
			    "for icon theme $theme"
		fi

		eval "cnt=\$guic_unexec_cnt_$varname"
		if (($cnt > 0)); then
			err "${portref}extra @unexec-delete of gtk-update-icon-cache" \
			    "for icon theme $theme"
			((guic_unexec_cnt--)) || true
		elif (($cnt < 0)); then 
			err "${portref}missing @unexec-delete of gtk-update-icon-cache" \
			    "for icon theme $theme"
		fi
	done

	for theme in $exec_icon_themes; do
		theme=${theme#/}
		theme=${theme%/}
		echo "$icon_themes" | fgrep -q "/$theme/" ||
			err "${portref}doing @exec of gtk-update-icon-cache" \
			    "for absent icon theme $theme"
	done

	for theme in $unexec_icon_themes; do
		theme=${theme#/}
		theme=${theme%/}
		echo "$icon_themes" | fgrep -q "/$theme/" ||
			err "${portref}doing @unexec-delete of gtk-update-icon-cache" \
			    "for absent icon theme $theme"
	done

	((guic_exec_cnt > 0)) &&
		err "${portref}extra @exec of gtk-update-icon-cache"
	((guic_unexec_cnt > 0)) &&
		err "${portref}extra @unexec-delete of gtk-update-icon-cache"

	# desktop-file-utils (simplier than previous, isn't it?)
	$mime_dep_needed && ! $mime_dep &&
	    [[ $fullpkgname != desktop-file-utils-* ]] &&
		err "${portref}missing RDEP on devel/desktop-file-utils"
	if $mime_dep_needed; then
		((mime_exec_cnt--)) || true
		((mime_unexec_cnt--)) || true
	fi
	if ((mime_exec_cnt > 0)) &&
	    [[ $fullpkgname != desktop-file-utils-* ]]; then
		err "${portref}extra @exec of update-desktop-database"
	elif ((mime_exec_cnt < 0)); then
		err "${portref}missing @exec of update-desktop-database"
	fi
	if ((mime_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of update-desktop-database"
	elif ((mime_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of update-desktop-database"
	fi

	# update-mime-database (same as previous)
	$mimepkg_dep_needed && ! $mimepkg_dep &&
	    [[ $fullpkgname != shared-mime-info-* ]] &&
		err "${portref}missing RDEP on misc/shared-mime-info"
	if $mimepkg_dep_needed; then
		((mimepkg_exec_cnt--)) || true
		((mimepkg_unexec_cnt--)) || true
	fi
	if ((mimepkg_exec_cnt > 0)) &&
	    [[ $fullpkgname != shared-mime-info-* ]]; then
		err "${portref}extra @exec of update-mime-database"
	elif ((mimepkg_exec_cnt < 0)); then
		err "${portref}missing @exec of update-mime-database"
	fi
	if ((mimepkg_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of update-mime-database"
	elif ((mimepkg_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of update-mime-database"
	fi

	# glib-compile-schemas (almost same as previous)
	#
	# TODO: detect situation of extra devel/dconf in MODULES
	# (requires investigation of all subpackages).
	if $dconf_dep_needed; then
		if ! $dconf_module; then
			err "${portref}GLib2 XML schemas found without" \
			    "devel/dconf in MODULES"
		elif ! $dconf_dep; then
			err "${portref}missing" \
			    "RUN_DEPENDS${subpkg}+=\${MODDCONF_RUN_DEPENDS}"
		fi
	fi

	if $dconf_dep_needed; then
		((dconf_exec_cnt--)) || true
		((dconf_unexec_cnt--)) || true
	fi
	if ((dconf_exec_cnt > 0)) &&
	    [[ $fullpkgname != glib2-* ]]; then
		err "${portref}extra @exec of glib-compile-schemas"
	elif ((dconf_exec_cnt < 0)); then
		err "${portref}missing @exec of glib-compile-schemas"
	fi
	if ((dconf_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of glib-compile-schemas"
	elif ((dconf_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of glib-compile-schemas"
	fi

	# gettext
	$translation_found && ! $gettext_dep && ! $is_static &&
		[[ $fullpkgname != gettext-* ]] &&
		err "${portref}translation file(s) found without" \
		    "devel/gettext dependency in RUN_DEPENDS"

	# dbus,-suid
	if $dbus_suid_dep_needed && ! $dbus_suid_dep; then
		err "${portref}missing" \
		    "RUN_DEPENDS${subpkg}+=x11/dbus,-suid"
	fi

	# Haskell register/unregister
	if $regsh && ! $regsh_exec; then
		err "${portref}missing @exec of register.sh"
	fi
	if $unregsh && ! $unregsh_exec; then
		err "${portref}missing @unexec of unregister.sh"
	fi

	# Python modules
	((${#py_files[@]} > 0))  && set -sA py_files  -- "${py_files[@]}"
	((${#pyc_files[@]} > 0)) && set -sA pyc_files -- "${pyc_files[@]}"
	((${#pyo_files[@]} > 0)) && set -sA pyo_files -- "${pyo_files[@]}"
	local ic=0 io=0
	if ((${#py_files[@]} > 0)); then for py in "${py_files[@]}"; do
		while [[ $ic -lt ${#pyc_files[@]} ]]; do
			[[ ${pyc_files[$ic]} < "$py"c ]] || break
			# allowed behaviour
			#err "${portref}compiled Python module without" \
			#    "source, expected: ${pyc_files[$ic]%c}"
			((++ic))
		done
		if [[ $ic -lt ${#pyc_files[@]} &&
		      ${pyc_files[$ic]} == "$py"c ]]; then
			((++ic))
		else
			err "${portref}Python module without" \
			    "compiled version, consider using" \
			    "\${MODPY_BIN} \${MODPY_LIBDIR}/compileall.py: $py"
		fi

		while [[ $io -lt ${#pyo_files[@]} ]]; do
			[[ ${pyo_files[$io]} < "$py"o ]] || break
			# allowed behaviour
			#err "${portref}optimized Python module without" \
			#    "source, expected: ${pyo_files[$io]%o}"
			((++io))
		done
		if [[ $io -lt ${#pyo_files[@]} &&
		      ${pyo_files[$io]} == "$py"o ]]; then
			((++io))
		# too much noise, maybe enable in the future
		#else
		#	err "${portref}Python module without" \
		#	    "optimized version: $py"
		fi
	done; fi

	# allowed behaviour
	#while (($ic < ${#pyc_files[@]})); do
	#	err "${portref}compiled Python module without source," \
	#	    "expected: ${pyc_files[$ic]%c}"
	#	((++ic))
	#done

	# allowed behaviour
	#while (($io < ${#pyo_files[@]})); do
	#	err "${portref}optimized Python module without source," \
	#	    "expected: ${pyo_files[$io]%o}"
	#	((++io))
	#done

	$wrong_man && err "${portref}manual pages should go under" \
	    "\${PREFIX}/man/ rather than under \${PREFIX}/share/man/"
	$wrong_info && err "${portref}info pages should go under" \
	    "\${PREFIX}/info/ rather than under \${PREFIX}/share/info/"
}

# Checks made:
#   * devel/gettext dependency is not forgotten.
#   * lib/kde/ and lib/kde4/ prefixes not missing where applicable.
#   * stdc++ doesn't get into WANTLIB when gcc4.port.mk is used.
check_wantlib() {
	local portref="$1"; shift
	local modules="$1"; shift

	local phonon_s_wantlib=false

	local kde3_module=false
	local kde4_module=false
	local phonon_module=false
	local gcc4_module=false

	local v

	for v in $modules; do case $v in
		gcc4)			gcc4_module=true;;
		multimedia/phonon)	phonon_module=true;;
		x11/kde)		kde3_module=true;;
		x11/kde4)		kde4_module=true;;
	esac; done

	for v; do case $v in
		phonon_s?(?(">")=+([0-9])))
			phonon_s_wantlib=true
			;;

		@(smbclient|wbclient)?(?('>')=+([0-9])))
			err "$portref$v instead of lib/samba/$v" \
			    "in WANTLIB"
			;;

		@(DCOP|soundserver_idl|vcard)?(?('>')=+([0-9])))
			err "$portref$v instead of \${KDE}/$v" \
			    "in WANTLIB (check other libs, too!)"
			;;

		@(kdecore|kdeui|kio)?(?('>')=+([0-9])))
			if $kde4_module; then
				err "$portref$v instead of \${KDE4LIB}/$v" \
				    "in WANTLIB (check other libs, too!)"
			elif $kde3_module; then 
				err "$portref$v instead of \${KDE}/$v" \
				    "in WANTLIB (check other libs, too!)"
			else
				err "$portref$v WANTLIB without x11/kde*" \
				    "in MODULES (check other libs, too!)"
			fi
			;;
		stdc++?(?('>')=+([0-9])))
			if $gcc4_module; then
				err "$portref$v in WANTLIB when gcc4 is" \
				    "in MODULES; run port-lib-depends-check" \
				    "and if stdc++ is still there, check" \
				    "actual build thoroughly, it's broken"
			fi
	esac; done

	if $phonon_s_wantlib && ! $phonon_module; then
		err "${portref}missing multimedia/phonon in MODULES"
	fi

	true
}

# Checks made:
#  * Each library mentioned in WANTLIB is accessible either:
#    a) as a part of base system, in /usr/lib or /usr/X11R6/lib;
#    b) via LIB_DEPENDS directly, or via deeper dependency of LIB_DEPENDS.
check_lib_depends() {
	$debugging && echo "CALLED: check_lib_depends($*)" >&2

	local portref="$1"; shift
	local subpkg="$1"; shift
	local modules="$1"; shift
	local wantlib="$1"; shift

	# The idea as follows: build full list of run-time dependencies, but
	# without RUN_DEPENDS begin involved.
	#
	# Then we look at libs in each pkgpath we got, and strip those
	# from WANTLIB. We also strip system libraries from /usr/lib
	# and /usr/X11R6/lib. And all WANTLIBs coming from MODULES are stripped
	# too, supposing that authors of those MODULES know what they're doing
	# (without stripping 'em, we'll get many false positives).
	#
	# If there are any non-stripped items in WANTLIB, we found a problem.
	#
	# XXX those checks do not take actual versions into account!

	# get list of all WANTLIBs coming from MODULES
	local m modvars=
	for m in $modules; do
		m=${m##*/}
		case $m in
		python)
			m=py
			;;
		esac
		m=$(echo "MOD${m}_WANTLIB" | tr a-z A-Z)
		modvars="$modvars $m"
	done
	local l modlibs=
	make "${make_args[@]}" show="$modvars" </dev/null |&
	while read -pr l; do
		modlibs="$modlibs $l"
	done
	wait    # make sure process exited before possible return below

	# strip WANTLIBs coming from MODULES
	local libsleft wl
	for l in $modlibs; do
		libsleft=
		for wl in $wantlib; do
			if [[ $l != "$wl" ]]; then
				libsleft="$libsleft $wl"
			elif $debugging; then
				echo "WANTLIB ITEM $wl COMES FROM MODULES"
			fi
		done
		[[ -n $libsleft ]] || return 0    # all libs found
		wantlib=$libsleft
	done

	# prepare easy-to-use WANTLIB list in $checklibs
	local wlprefix checklibs=
	for wl in $wantlib; do
		wl=${wl%%[><=]*}
		wlprefix=${wl%/*}
		[[ $wlprefix == "$wl" ]] && wlprefix=lib
		wl=${wl##*/}
		checklibs="$checklibs ${wlprefix}/lib$wl"
	done

	# strip system libraries
	local d
	for d in /usr /usr/X11R6; do
		for l in $d/lib/lib*.@(so*(.+([0-9]))|a); do
			libsleft=
			for wl in $checklibs; do
				if [[ $l != +(/*)/${wl}.@(so*(.+([0-9]))|a) ]]; then
					libsleft="$libsleft $wl"
				elif $debugging; then
					echo "FOUND WANTLIB ITEM $wl: $l"
				fi
			done
			[[ -n $libsleft ]] || return 0    # all libs found
			checklibs=$libsleft
		done
	done

	# get deep list of LDEPs
	local lmake_args="${make_args[@]}"
	lmake_args[${#lmake_args[@]}]="RUN_DEPENDS="
	lmake_args[${#lmake_args[@]}]="RUN_DEPENDS$subpkg="
	# Rely on the fact we're already in the port directory, see sub_checks().
	# XXX ignoring make errors for now
	local pure_lib_deps=$(make "${lmake_args[@]}" show-run-depends | sort)
	[[ -n $pure_lib_deps ]] || return 0
	# SUBDIR doesn't accept newline-separated values
	set -A pure_lib_deps -- $pure_lib_deps

	(
	# strip libraries from ports
	# TODO cache print-plist-libs output?
	cd -- /usr/ports    # XXX "$portsdir" fails for openbsd-wip and like
	unset FLAVOR SUBPACKAGE
	make "${make_args[@]}" SUBDIR="${pure_lib_deps[*]}" \
	    print-plist-libs </dev/null 2>/dev/null |&
	while read -pr l; do
		case $l in
		"===> "*)
			;;

		*)
			libsleft=
			for wl in $checklibs; do
				if [[ $l != +(/*)/${wl}.@(so*(.+([0-9]))|a) ]]; then
					libsleft="$libsleft $wl"
				elif $debugging; then
					echo "FOUND WANTLIB ITEM $wl: $l"
				fi
			done
			[[ -n $libsleft ]] || exit 0    # all libs found
			checklibs=$libsleft
			;;
		esac
	done

	# prettify list of WANTLIBs left and print it
	libsleft=
	for wl in $checklibs; do
		libsleft="$libsleft ${wl##*/lib}"
	done
	err "${portref}the following libraries in WANTLIB${subpkg%-}" \
	    "look like masked by RUN_DEPENDS${subpkg%-}:$libsleft"
	wait
	! $error
	) || error=true
}

# Checks made:
#   * No extra PERMIT_DISTFILES_FTP variables in Makefile.
#   * PERMIT_DISTFILES_FTP should not contain just "No" but a reason.
#
# Runs in the port directory.
# XXX does not handle Makefile.inc and other .include cases correctly.
check_permit_dist() {
	$debugging && echo "CALLED: check_permit_dist($*)" >&2

	local portref=$(portref $1); shift
	local perm_pkg_cdrom=$(echo "$1" | tr '[:upper:]' '[:lower:]')
	local perm_pkg_ftp=$(echo "$2" | tr '[:upper:]' '[:lower:]')
	local perm_dist_ftp=$(echo "$3" | tr '[:upper:]' '[:lower:]')

	if [[ ($perm_pkg_cdrom == yes || $perm_pkg_ftp == yes) && \
	    $perm_dist_ftp == yes ]]; then
		egrep -sq "^ *PERMIT_DISTFILES_FTP[[:space:]]*=" Makefile &&
			err "${portref}extra PERMIT_DISTFILES_FTP line(-s)"
	fi

	if [[ $perm_dist_ftp == no ]]; then
		err "${portref}PERMIT_DISTFILES_FTP should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi

	true
}

# Checks made:
#   * No extra PERMIT_PACKAGE_FTP variables in Makefile.
#   * PERMIT_PACKAGE_* should not contain just "No" but a reason.
#
# Runs in the port directory.
# XXX does not handle Makefile.inc and other .include cases correctly.
check_permit_subpkg() {
	$debugging && echo "CALLED: check_permit_subpkg($*)" >&2

	local portref=$1; shift
	local subpkg=${1%-}; shift
	local perm_pkg_cdrom=$(echo "$1" | tr '[:upper:]' '[:lower:]')
	local perm_pkg_ftp=$(echo "$2" | tr '[:upper:]' '[:lower:]')

	if [[ $perm_pkg_cdrom == yes && $perm_pkg_ftp == yes ]]; then
		egrep -q "^ *PERMIT_PACKAGE_FTP${subpkg}[[:space:]]*=" Makefile &&
			err "${portref}extra PERMIT_PACKAGE_FTP lines"
	fi

	if [[ $perm_pkg_cdrom == no ]]; then
		err "${portref} PERMIT_PACKAGE_CDROM should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi
	if [[ $perm_pkg_ftp == no ]]; then
		err "${portref} PERMIT_PACKAGE_FTP should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi

	true
}

# Checks made:
#   * Directory is not empty
#   * No '*.core' files present
check_files_dir() {
	$debugging && echo "CALLED: check_files_dir($*)" >&2

	find -f "$1" -- -type f | {
		local empty=true
		local mode
		while read F; do
			ignoring "$F" && continue
			mode=$(stat -f %p "$F" || true)
			(( (0$mode & 0111) != 0 )) &&
				err "executable file: $F"
			empty=false
			[[ $F == *.core ]] &&
				err_coredump_found "$F"
		done
		$empty && err "there are no files, please remove the $1 directory"
		! $error
	} || error=true
}

# Checks made:
#   * The patch is not empty.
#   * The patch contains an OpenBSD RCS tag.
check_patch() {
	local F=$1

	test -f "$F" || {
		err "$F is not a file"
		return
	}

	if [ -s "$F" ]; then
		head -n 1 -- "$F" | egrep -q '^\$OpenBSD.*\$$' ||
		    err "$F does not have \$OpenBSD\$ RCS tag at the top"
	else
		err "$F is empty and should be removed"
	fi
}

# Checks made:
#   * Each patch contains OpenBSD RCS tag.
#   * Directory is not empty and consists only of plain files starting
#     with 'patch-' and not ending with '.orig'.
check_patches_dir() {
	$debugging && echo "CALLED: check_patches_dir($*)" >&2

	local empty=true
	local F

	check_perms_in_dir "$1"

	for F in "$1"/* "$1"/.*; do case "${F##*/}" in
	patch-*.orig)
		handle_extra_file "$F"
		;;

	patch-*)
		empty=false
		$rootrun || check_patch "$F"
		;;

	*)
		handle_extra_file "$F"
		;;
	esac; done

	$empty && err "there are no patches, please remove the $1 directory instead"
}

# Checks made:
#   * Directory is not empty and consist only of plain files with fixed names.
#   * PFRAG, PLIST, README and .rc files contain appropriate OpenBSD RCS
#     tags; other files should NOT contain OpenBSD RCS tag.
#   * PFRAG.shared should be merged into PLIST.
#   * No trailing whitespace for DESCR, MESSAGE, README, UNMESSAGE and
#     .rc files (PLIST and PFRAG are better checked with "make package").
#   * See also check_plist_file().
check_pkg_dir() {
	$debugging && echo "CALLED: check_pkg_dir($*)" >&2

	local dir=$1; shift
	local subst_cmd
	if (($# > 0)); then
		# XXX should find the way to always obtain SUBST_CMD
		subst_cmd=$1
		shift
	fi
	local empty=true
	local F
	local plist

	check_perms_in_dir "$dir"

	dir="${dir#./}"
	for F in "$dir"/* "$dir"/.*; do case "${F##*/}" in
	DESCR?(-*))
		empty=false
		[[ -f $F ]] ||
			err "$F is not a file"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		egrep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

	PFRAG.shared?(-*))
		empty=false
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_plist_file "$F"
		plist=PLIST${F##*/PFRAG.+([!-])}
		err "$F should be merged into $plist"
		;;

	PFRAG.*|PLIST?(-*))
		empty=false
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_plist_file "$F"
		;;

	README?(-*))
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		head -n 1 -- "$F" |
			egrep -q '^(#[[:space:]]*)?\$OpenBSD(:.*)?\$$' ||
			err "$F does not have \$OpenBSD\$ RCS tag at the top"
		;;

	*.rc)
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ ${F##*/} == [A-Za-z_]*([A-Za-z0-9_]).rc ]] ||
			err "$F name will not work in rc.subr(8)"
		check_trailing_whitespace "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		head -n 5 -- "$F" |
			egrep -q '^#[[:space:]]*\$OpenBSD(:.*)?\$$' ||
			err "$F does not have \$OpenBSD\$ RCS tag at the top"
		;;

	MESSAGE?(-*)|UNMESSAGE?(-*))
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		egrep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

	*)
		handle_extra_file "$F"
		;;
	esac; done

	$empty && err "$dir directory does not contain either DESCR, PFRAG or PLIST files"
}

# Checks made:
#   * There are no hardcoded /usr/local or /var paths in file.
#     /var/log, /var/run and /var/tmp are perfectly fine, though.
check_hardcoded() {
	$debugging && echo "CALLED: check_hardcoded($*)" >&2

	perl -n -e 'BEGIN { $ec=0; }
		    if (m,/usr/local\b,o) { $ec=1; close ARGV; }
		    if (m,/var((?:/+[^/\s]+)*)(?:\s.*)?$,o) {
			unless ($1 =~ m,^/+(?:log|run|tmp),o) {
			    $ec=1; close ARGV;
			}
		    }
		    END { $? = $ec; }' \
		"$1" || err "hardcoded paths detected in $1, consider using" \
		    "SUBST_VARS and TRUEPREFIX/LOCALBASE/LOCALSTATEDIR/VARBASE"
}

# Checks made:
#   * There are no lines longer than 80 characters that have at least
#     one space (avoids warnings on long URLs etc.).
check_long_lines() {
	$debugging && echo "CALLED: check_long_lines($*)" >&2
	local file=$1; shift

	local n=$(awk <"$file" \
		  '/[[:space:]]/ && length > 80 { n++ } END { print n+0 }')
	(($n > 0 )) &&
		err "$n line(s) longer than 80 chars in $file"
}

# Checks made:
#   * There is an OpenBSD RCS tag at the top.
#   * No items with ${FULLPKGNAME} are allowed, except readme.
#   * No empty lines.
check_plist_file() {
	$debugging && echo "CALLED: check_plist_file($*)" >&2

	[[ -f $1 ]] ||
		err "$1 is not a file"
	head -n 1 -- "$1" |
		egrep -q '^@comment \$OpenBSD.*\$$' ||
		err "$1 does not have \$OpenBSD\$ RCS tag at the top"

	# Do not match just '${FULLPKGNAME}' because many ports use the
	# following trick:
	#    @cwd ${LOCALBASE}/share/doc/pkg-readmes
	#    ${FULLPKGNAME}
	egrep -v '^(share/doc/pkg-readmes/\$\{FULLPKGNAME\}|@comment .*)$' "$1" |
		egrep '.\$\{FULLPKGNAME\}|\$\{FULLPKGNAME\}.' >&2 &&
		err "$1 contains item(s) with \${FULLPKGNAME} in it, see above"

	egrep -q '^[[:space:]]*$' "$1" && err "$1 contains empty lines"
}

# Checks made:
#   * Every variable referenced by ${[A-Z]+} should be in ${SUBST_VARS}.
check_subst_vars() {
	$debugging && echo "CALLED: check_subst_vars($*)" >&2

	local F=$1; shift
	local subst_cmd=$1; shift

	# Add variables sometimes referenced in port docs.
	eval "$subst_cmd" -DPATH=test -DWRKSRC=test <"$F" |
	    egrep '\$\{[A-Z]+\}' >&2 &&
		err "looks like misspelled variables in $F, see above"
}

# Checks made:
#   * Contains OpenBSD RCS tag at the top line.
#   * No REVISION marks present in given file (unless in update mode).
#   * Each REVISION mark presents only once.
#   * BUILD_DEPENDS, MODULES and PERMIT_DISTFILES_FTP are not defined in
#     VAR-subpkg manner.
#   * No trailing whitespace.
#   * SHARED_LIBS are not defined inside ".if" statements.
#   * Variables are not assigned via "=" twice outside of .if statemets.
#   * No user settings present.
#   * SHARED_ONLY not defined
check_makefile() {
	$debugging && echo "CALLED: check_makefile($*)" >&2

	local F="$1"
	check_trailing_whitespace "$F"
	check_long_lines "$F"
	check_hardcoded "$F"
	head -n 1 -- "$F" |
		egrep -q '^#[[:space:]]*\$OpenBSD.*\$' ||
		err "$F does not have \$OpenBSD\$ RCS tag at the top"

	local iflevel=0 l lnum=0 revs= t r mkvars= var duprevfound
	# do not unset mkvars, having empty element(-s) is fine
	unset revs[0]
	local tab="$(print '\t')"
	while IFS= read -r l; do ((++lnum))
		set -A t -- $l
		duprevfound=false

		if echo "$l" | egrep -q "^ *($user_settings)[[:>:]].*\$"; then
			err "user settings in port: $l"
		fi

		case $l in
		*(" ")REVISION*)
			$existing_port ||
				err "REVISION mark found at $F:$lnum"
			var=${t[0]%=}
			if ((${#revs[@]} > 0)); then
				for r in "${revs[@]}"; do
					if [[ $var == "$r" ]]; then
						err "duplicated $r in $F"
						# avoid dup error messages
						duprevfound=true
						break
					fi
				done
			fi
			revs[${#revs[@]}]=${t[0]}
			;;
		*(" ")@(BUILD_DEPENDS|MODULES|PERMIT_DISTFILES_FTP)-*)
			err "${l%%-*} is not a subpackageble variable, see $F:$lnum"
			;;
		*(" ").*(" "|"$tab")if*)
			((++iflevel))
			;;
		*(" ").*(" "|"$tab")endif*)
			((iflevel--))
			;;
		*(" ")SHARED_LIBS*(" "|"$tab")*(+|:|!)=*)
			if ((iflevel > 0)); then
				err "should not be inside .if block ($F:$lnum): $l"
			fi
			;;
		*(" ")SHARED_ONLY*(" "|"$tab")*(+|:|!|\?)=*)
			err "SHARED_ONLY is deprecated ($F:$lnum)"
			;;
		esac

		if [[ $l == *(" ")+([A-Za-z0-9_-])*(" "|"$tab")?(\?)=* ]] &&
		   ((iflevel == 0)) && ! $duprevfound; then
			var=${t[0]%?(\?)=*}
			for v in "${mkvars[@]}"; do
				if [[ $v == "$var" ]]; then
					err "duplicated assignment of $v" \
					    "at $F:$lnum"
					break
				fi
			done
			mkvars[${#mkvars[@]}]=$var
		fi
	done <"$F"
}

# Checks made:
#   * None of executable bits (111) are set on plain files.
check_perms_in_dir() {
	$debugging && echo "CALLED: check_perms_in_dir($*)" >&2

	(find -f "$1" -- -maxdepth 1 -type f \
	    \( -perm -100 -or -perm -010 -or -perm 001 \) \
	    </dev/null || true) |&
	local F
	while read -pr F; do
		F=${F#./}
		ignoring "$F" && continue
		err "executable file: ${F#./}"
	done
}


############################################################
# Run checks. Also calculate and show pkgpath variable,
# unless we're checking the ports tree root dir.
#

for D; do
	if [[ $D == /* ]]; then
		err "absolute path $D ignored"
		continue
	fi
	if [[ $D == *(*/)..*(/*) ]]; then
		err "too many .. in $D, skipping"
		continue
	fi
	check_port_dir "$D"
done

if ! $rootrun; then
	[[ -z $pkgpath ]] && pkgpath=${PWD##"$portsdir/"}

	if [[ $pkgpath == "$PWD" ]]; then
		cat >&2 <<EOE
${0##*/}: could not determine PKGPATH. Please help me with the -p option.
EOE
		exit 2
	fi

	echo "$pkgpath"
fi

! $error
